// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Common errors generated by the components in this crate.

use http::StatusCode;
use std::error::Error;

pub use gax::error::CredentialsError;

/// Represents an error using [SubjectTokenProvider].
///
/// The Google Cloud client libraries may experience problems when
/// fetching the subject token. For example, a temporary error may occur
/// when [SubjectTokenProvider] tries to fetch the token from a third party service.
///
/// Applications rarely need to create instances of this error type. The
/// exception may be when they are providing their own custom [SubjectTokenProvider]
/// implementation.
///
/// # Example
///
/// ```
/// # use std::error::Error;
/// # use std::fmt;
/// # use google_cloud_auth::errors::SubjectTokenProviderError;
/// #[derive(Debug)]
/// struct CustomTokenError {
///     message: String,
///     is_transient: bool,
/// }
///
/// impl fmt::Display for CustomTokenError {
///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
///         write!(f, "{}", self.message)
///     }
/// }
///
/// impl Error for CustomTokenError {}
///
/// impl SubjectTokenProviderError for CustomTokenError {
///     fn is_transient(&self) -> bool {
///         self.is_transient
///     }
/// }
/// ```
/// [SubjectTokenProvider]: crate::credentials::subject_token::SubjectTokenProvider
pub trait SubjectTokenProviderError: Error + Send + Sync + 'static {
    /// Return true if the error is transient and the call may succeed in the future.
    ///
    /// Applications should only return true if the error automatically
    /// recovers, without the need for any human action.
    ///
    /// Timeouts and network problems are good candidates for `is_transient() == true`.
    /// Configuration errors that require changing a file, or installing an executable are not.
    fn is_transient(&self) -> bool;
}

impl SubjectTokenProviderError for CredentialsError {
    fn is_transient(&self) -> bool {
        self.is_transient()
    }
}

pub(crate) fn from_http_error(err: reqwest::Error, msg: &str) -> CredentialsError {
    let transient = self::is_retryable(&err);
    CredentialsError::new(transient, msg, err)
}

pub(crate) async fn from_http_response(response: reqwest::Response, msg: &str) -> CredentialsError {
    let err = response
        .error_for_status_ref()
        .expect_err("this function is only called on errors");
    let body = response.text().await;
    let transient = crate::errors::is_retryable(&err);
    match body {
        Err(e) => CredentialsError::new(transient, msg, e),
        Ok(b) => CredentialsError::new(transient, format!("{msg}, body=<{b}>"), err),
    }
}

/// A helper to create a non-retryable error.
pub(crate) fn non_retryable<T: Error + Send + Sync + 'static>(source: T) -> CredentialsError {
    CredentialsError::from_source(false, source)
}

pub(crate) fn non_retryable_from_str<T: Into<String>>(message: T) -> CredentialsError {
    CredentialsError::from_msg(false, message)
}

fn is_retryable(err: &reqwest::Error) -> bool {
    // Connection errors are transient more often than not. A bad configuration
    // can point to a non-existing service, and that will never recover.
    // However: (1) we expect this to be rare, and (2) this is what limiting
    // retry policies and backoff policies handle.
    if err.is_connect() {
        return true;
    }
    match err.status() {
        Some(code) => is_retryable_code(code),
        None => false,
    }
}

fn is_retryable_code(code: StatusCode) -> bool {
    match code {
        // Internal server errors do not indicate that there is anything wrong
        // with our request, so we retry them.
        StatusCode::INTERNAL_SERVER_ERROR
        | StatusCode::SERVICE_UNAVAILABLE
        | StatusCode::REQUEST_TIMEOUT
        | StatusCode::TOO_MANY_REQUESTS => true,
        _ => false,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::num::ParseIntError;
    use test_case::test_case;

    #[test_case(StatusCode::INTERNAL_SERVER_ERROR)]
    #[test_case(StatusCode::SERVICE_UNAVAILABLE)]
    #[test_case(StatusCode::REQUEST_TIMEOUT)]
    #[test_case(StatusCode::TOO_MANY_REQUESTS)]
    fn retryable(c: StatusCode) {
        assert!(is_retryable_code(c));
    }

    #[test_case(StatusCode::NOT_FOUND)]
    #[test_case(StatusCode::UNAUTHORIZED)]
    #[test_case(StatusCode::BAD_REQUEST)]
    #[test_case(StatusCode::BAD_GATEWAY)]
    #[test_case(StatusCode::PRECONDITION_FAILED)]
    fn non_retryable(c: StatusCode) {
        assert!(!is_retryable_code(c));
    }

    #[test]
    fn helpers() {
        let e = super::non_retryable_from_str("test-only-err-123");
        assert!(!e.is_transient(), "{e}");
        let got = format!("{e}");
        assert!(got.contains("test-only-err-123"), "{got}");

        let input = "NaN".parse::<u32>().unwrap_err();
        let e = super::non_retryable(input.clone());
        assert!(!e.is_transient(), "{e:?}");
        let source = e.source().and_then(|e| e.downcast_ref::<ParseIntError>());
        assert!(matches!(source, Some(ParseIntError { .. })), "{e:?}");
    }
}
